package net.sf.nwn.jme3;

import java.lang.ref.WeakReference;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import net.sf.nwn.jme3.SkinmeshGeomNode.VertexWeight;

import com.jme3.asset.AssetManager;
import com.jme3.material.Material;
import com.jme3.material.RenderState.BlendMode;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue.Bucket;
import com.jme3.renderer.queue.RenderQueue.ShadowMode;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial.BatchHint;
import com.jme3.scene.VertexBuffer.Type;
import com.jme3.texture.Texture;
import com.jme3.util.BufferUtils;

public class TrimeshGeomNode extends DummyGeomNode {

    private ColorRGBA ambient;
    private ColorRGBA diffuse;
    private ColorRGBA specular;
    private ColorRGBA emissive;
    private float shininess;
    protected ArrayList<Vector3f> verts = new ArrayList<Vector3f>();
    private ArrayList<Face> faces = new ArrayList<Face>();
    private ArrayList<Vector2f> tverts = new ArrayList<Vector2f>();
    private ArrayList<ColorRGBA> colors = new ArrayList<ColorRGBA>();
    private ArrayList<Vector3f> normals = new ArrayList<Vector3f>();
    protected ArrayList<Float> constraints;
    protected List<VertexWeight[]> vertexWeights;
    private String bitmap;
    private float alpha = 1;
    private boolean render = true;
    private int transparencyHint = 0;
    private WeakReference<Mesh> cachedGeometry;
    private WeakReference<Material> cachedMaterial;
    private BatchHint batchHint = null;

    public TrimeshGeomNode(GeomNode parent) {
        super(parent);
    }

    @Override
    public String getType() {
        return "trimesh";
    }

    /**
     * Gets the ambient.
     * 
     * @return Returns a ColorRGBA
     */
    public ColorRGBA getAmbient() {
        return ambient;
    }

    /**
     * Sets the ambient.
     * 
     * @param ambient
     *            The ambient to set
     */
    public void setAmbient(ColorRGBA ambient) {
        this.ambient = ambient;
    }

    /**
     * Gets the diffuse.
     * 
     * @return Returns a ColorRGBA
     */
    public ColorRGBA getDiffuse() {
        return diffuse;
    }

    /**
     * Sets the diffuse.
     * 
     * @param diffuse
     *            The diffuse to set
     */
    public void setDiffuse(ColorRGBA diffuse) {
        this.diffuse = diffuse;
    }

    /**
     * Gets the shininess.
     * 
     * @return Returns a float
     */
    public float getShininess() {
        return shininess;
    }

    /**
     * Sets the shininess.
     * 
     * @param shininess
     *            The shininess to set
     */
    public void setShininess(float shininess) {
        this.shininess = shininess;
    }

    /**
     * Gets the specular.
     * 
     * @return Returns a ColorRGBA
     */
    public ColorRGBA getSpecular() {
        return specular;
    }

    /**
     * Sets the specular.
     * 
     * @param specular
     *            The specular to set
     */
    public void setSpecular(ColorRGBA specular) {
        this.specular = specular;
    }

    public void addVert(Vector3f vert) {
        verts.add(vert);
    }

    public void addTvert(Vector2f tvert) {
        tverts.add(tvert);
    }

    public void addFace(int v1, int v2, int v3, int m1, int t1, int t2, int t3, int m2) {
        Face f = new Face();

        f.setV1(v1);
        f.setV2(v2);
        f.setV3(v3);
        f.setM1(m1);
        f.setT1(t1);
        f.setT2(t2);
        f.setT3(t3);
        f.setM2(m2);
        faces.add(f);
    }

    public void addColor(ColorRGBA color) {
        colors.add(color);
    }

    /**
     * Gets the bitmap.
     * 
     * @return Returns a String
     */
    public String getBitmap() {
        return bitmap;
    }

    /**
     * Sets the bitmap.
     * 
     * @param bitmap
     *            The bitmap to set
     */
    public void setBitmap(String bitmap) {
        this.bitmap = bitmap;
    }
    Vector2f zeroTvert = new Vector2f(0, 0);

    public Vector2f getTvert(int index) {
        if (index < 0) {
            System.err.println("WARN: Negative tvert for part " + getName());
            return zeroTvert;
        }
        if (index >= tverts.size()) {
            System.err.println("WARN: Requested tvert index out of range " + index + " >= " + tverts.size() + " for part " + getName());
            return zeroTvert;
        }
        return (Vector2f) tverts.get(index);
    }

    @Override
    public void dumpSingle(StringBuffer sb) {
        super.dumpSingle(sb);

        if (ambient != null) {
            sb.append("  ambient ");
            dump(sb, ambient);
            sb.append("\n");
        }

        if (diffuse != null) {
            sb.append("  diffuse ");
            dump(sb, diffuse);
            sb.append("\n");
        }

        if (specular != null) {
            sb.append("  specular ");
            dump(sb, specular);
            sb.append("\n");
        }

        if (emissive != null && emissive != black) {
            sb.append(" selfillumcolor ");
            dump(sb, emissive);
            sb.append("\n");
        }

        sb.append("  shininess ").append(shininess).append("\n");

        if (bitmap != null) {
            sb.append("  bitmap ").append(bitmap).append("\n");
        }

        sb.append("  verts ").append(verts.size()).append("\n");
        for (int i = 0; i < verts.size(); i++) {
            sb.append("    ");
            dump(sb, (Vector3f) verts.get(i));
            sb.append("\n");
        }

        sb.append("  faces ").append(faces.size()).append("\n");
        for (int i = 0; i < faces.size(); i++) {
            sb.append("    ");
            sb.append(faces.get(i));
            sb.append("\n");
        }

        sb.append("  tverts ").append(tverts.size()).append("\n");
        for (int i = 0; i < tverts.size(); i++) {
            sb.append("    ");
            dump(sb, (Vector2f) tverts.get(i));
            sb.append(" 0\n");
        }

        if (colors.size() > 0) {
            sb.append("  colors ").append(colors.size()).append("\n");
            for (int i = 0; i < colors.size(); i++) {
                sb.append("    ");
                dump(sb, (ColorRGBA) colors.get(i));
                sb.append("\n");
            }
        }
    }

    // ------------------------------------------------
    static class Vertex {

        public Vector3f p;
        public Vector2f t;
        public int group;

        public Vertex(Vector3f ap, Vector2f at, int aGroup) {
            p = ap;
            t = at;
            group = aGroup;
        }

        public boolean equals(Vertex v) {
            return p.equals(v.p) && t.equals(v.t) && group == v.group;
        }

        @Override
        public boolean equals(Object o) {
            return equals((Vertex) o);
        }

        @Override
        public int hashCode() {
            return p.hashCode() ^ t.hashCode();
        }
    }

    public void indexify() {
        Map<Vertex, Integer> map = new HashMap<Vertex, Integer>();
        ArrayList<Vector3f> nverts = new ArrayList<Vector3f>();
        ArrayList<Vector2f> ntverts = new ArrayList<Vector2f>();
        ArrayList<ColorRGBA> ncolors = new ArrayList<ColorRGBA>();
        ArrayList<Float> nconstraints = null;
        ArrayList<VertexWeight[]> nweights = null;

        if (constraints != null) {
            nconstraints = new ArrayList<Float>();
        }

        if (vertexWeights != null) {
            nweights = new ArrayList<SkinmeshGeomNode.VertexWeight[]>();
            batchHint = BatchHint.Never;
        }

        normals.clear();

        ArrayList<Vector3f> normalP = new ArrayList<Vector3f>();
        ArrayList<Vector3f> normalV = new ArrayList<Vector3f>();
        ArrayList<Integer> normalG = new ArrayList<Integer>();

        int count = 0;

        for (int i = 0; i < faces.size(); i++) {
            Face f = (Face) faces.get(i);

            // compute face normal
            Vector3f p1 = (Vector3f) verts.get(f.getV1());
            Vector3f p2 = (Vector3f) verts.get(f.getV2());
            Vector3f p3 = (Vector3f) verts.get(f.getV3());
            Vector3f a = p3.subtract(p2);
            Vector3f b = p1.subtract(p2);
            Vector3f normal = a.cross(b);

            normal = normal.normalize();

            if (Float.isNaN(normal.x)) {
                System.err.println("WARN: Illegal normal for part " + getName());
                System.err.println(" face " + i);
                System.err.println(" p1=" + f.getV1() + "(" + p1 + ")");
                System.err.println(" p2=" + f.getV2() + "(" + p2 + ")");
                System.err.println(" p3=" + f.getV3() + "(" + p3 + ")");
                normal.x = 1.0f;
                normal.y = 0;
                normal.z = 0;
            }

            normalP.add(p1);
            normalV.add(normal);
            normalP.add(p2);
            normalV.add(normal);
            normalP.add(p3);
            normalV.add(normal);
            Integer groupI = new Integer(f.getM1());

            normalG.add(groupI);
            normalG.add(groupI);
            normalG.add(groupI);

            Vector3f p = (Vector3f) verts.get(f.getV1());
            Vector2f t = getTvert(f.getT1());
            Vertex v = new Vertex(p, t, f.getM1());
            Integer index = (Integer) map.get(v);

            if (index == null) {
                index = new Integer(count);
                count++;
                map.put(v, index);
                nverts.add(p);
                ntverts.add(t);
                normals.add(normal);
                if (colors.size() > 0) {
                    ncolors.add(colors.get(f.getV1()));
                }
                if (constraints != null) {
                    nconstraints.add(constraints.get(f.getV1()));
                }
                if (vertexWeights != null) {
                    nweights.add(vertexWeights.get(f.getV1()));
                }
            }
            f.setV1(index.intValue());
            f.setT1(index.intValue());

            p = (Vector3f) verts.get(f.getV2());
            t = getTvert(f.getT2());
            v = new Vertex(p, t, f.getM1());
            index = (Integer) map.get(v);
            if (index == null) {
                index = new Integer(count);
                count++;
                map.put(v, index);
                nverts.add(p);
                ntverts.add(t);
                normals.add(normal);
                if (colors.size() > 0) {
                    ncolors.add(colors.get(f.getV2()));
                }
                if (constraints != null) {
                    nconstraints.add(constraints.get(f.getV2()));
                }

                if (vertexWeights != null) {
                    nweights.add(vertexWeights.get(f.getV2()));
                }
            }
            f.setV2(index.intValue());
            f.setT2(index.intValue());

            p = (Vector3f) verts.get(f.getV3());
            t = getTvert(f.getT3());
            v = new Vertex(p, t, f.getM1());
            index = (Integer) map.get(v);
            if (index == null) {
                index = new Integer(count);
                count++;
                map.put(v, index);
                nverts.add(p);
                ntverts.add(t);
                normals.add(normal);
                if (colors.size() > 0) {
                    ncolors.add(colors.get(f.getV3()));
                }
                if (constraints != null) {
                    nconstraints.add(constraints.get(f.getV3()));
                }

                if (vertexWeights != null) {
                    nweights.add(vertexWeights.get(f.getV3()));
                }
            }
            f.setV3(index.intValue());
            f.setT3(index.intValue());
        }

        verts = nverts;
        tverts = ntverts;
        colors = ncolors;
        constraints = nconstraints;
        vertexWeights = nweights;

        ArrayList<Vector3f> found = new ArrayList<Vector3f>();

        while (normalP.size() > 0) {
            found.clear();
            Vector3f p = (Vector3f) normalP.get(normalP.size() - 1);
            Integer g = (Integer) normalG.get(normalG.size() - 1);
            Iterator<Vector3f> pi = normalP.iterator();
            Iterator<Vector3f> vi = normalV.iterator();
            Iterator<Integer> gi = normalG.iterator();

            while (pi.hasNext()) {
                Vector3f currentP = (Vector3f) pi.next();
                Vector3f currentV = (Vector3f) vi.next();
                Integer currentI = (Integer) gi.next();

                if (currentP.equals(p)/* && currentI.equals(g) */) {
                    pi.remove();
                    vi.remove();
                    gi.remove();
                    found.add(currentV);
                }
            }

            Vector3f normal = new Vector3f();

            for (int i = 0; i < found.size(); i++) {
                normal = normal.add((Vector3f) found.get(i));

            }
            normal = normal.normalize();
            if (Float.isNaN(normal.x)) {
                System.err.println("Illegal normal");
                normal.x = 1.0f;
                normal.y = 0;
                normal.z = 0;
            }

            for (int index = 0; index < verts.size(); index++) {
                if (!p.equals(verts.get(index))) {
                    continue;
                }
                normals.set(index, normal);
            }
        }
    }

    public Mesh createGeometry(ShadingType metallic) {
        return createGeometry(true, metallic);
    }
    static int icount = 0;

    public Mesh createGeometry(boolean strip, ShadingType metallic) {

        if (cachedGeometry != null) {
            Mesh ga = cachedGeometry.get();

            if (ga != null) {
                return ga;
            }
        }

        indexify();

        Mesh mesh = new Mesh();


        Vector3f[] coords = new Vector3f[verts.size()];

        verts.toArray(coords);

        FloatBuffer coordBuffer = BufferUtils.createFloatBuffer(coords);

        mesh.setBuffer(Type.Position, 3, coordBuffer);

        int[] vIndices = new int[faces.size() * 3];

        for (int i = 0; i < faces.size(); i++) {
            Face f = (Face) faces.get(i);

            vIndices[i * 3] = f.getV1();
            vIndices[i * 3 + 1] = f.getV2();
            vIndices[i * 3 + 2] = f.getV3();
        }

        IntBuffer indexBuffer = BufferUtils.createIntBuffer(vIndices);

        mesh.setBuffer(Type.Index, 3, indexBuffer);

        if (colors.size() > 0) {

            FloatBuffer colorBuffer = BufferUtils.createFloatBuffer(colors.size() * 4);

            for (ColorRGBA color : colors) {
                colorBuffer.put(color.r);
                colorBuffer.put(color.g);
                colorBuffer.put(color.b);
                colorBuffer.put(color.a);
            }

            mesh.setBuffer(Type.Color, 4, colorBuffer);

        }



        Vector2f[] texes = new Vector2f[tverts.size()];
        tverts.toArray(texes);
        FloatBuffer texBuffer = BufferUtils.createFloatBuffer(texes);
        mesh.setBuffer(Type.TexCoord, 2, texBuffer);

        Vector3f[] normalsA = new Vector3f[normals.size()];
        normals.toArray(normalsA);
        FloatBuffer normalBuffer = BufferUtils.createFloatBuffer(normalsA);
        mesh.setBuffer(Type.Normal, 3, normalBuffer);


        /*
         * if (strip) { Stripifier st = new Stripifier();
         * 
         * st.stripify(gi); }
         */

//		mesh.setVertexCount(verts.size());
//		mesh.setTriangleCount(faces.size());
        mesh.setStatic();
        mesh.updateBound();


        cachedGeometry = new WeakReference<Mesh>(mesh);
        return mesh;

    }
    public static final ColorRGBA black = new ColorRGBA(0, 0, 0, 1);
    public static final ColorRGBA white = new ColorRGBA(1, 1, 1, 1);

    public Material createAppearance(AssetManager base, ShadingType metallic) {

        Material mat = null;
        if (cachedMaterial != null) {
            mat = cachedMaterial.get().clone();
        } else {
            mat = MaterialManager.getInstance().getMaterial(bitmap);
        }
        if (mat != null) {
            return mat;
        }

        if (metallic == ShadingType.METALLIC && transparencyHint == 0) {
            mat = new Material(base, "net/sf/nwn/jme3/shader/Metallic.j3md");
            Texture chromeMap = findTexture(base, "chrome1.tga");
            mat.setTexture("EnvMap", chromeMap);
        } else {
            mat = new Material(base, "Common/MatDefs/Light/Lighting.j3md");

        }
        if (metallic == ShadingType.LOW) {
            mat.setBoolean("VertexLighting", true);
            mat.setBoolean("LowQuality", true);
        }


        if (transparencyHint > 0) {
            mat.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
        }


        Texture tex_ml = findTexture(base, bitmap);
        System.out.println(bitmap);
        if (tex_ml == null) {
            System.out.println("Cannot find texture " + bitmap);
        } else {
            mat.setTexture("DiffuseMap", tex_ml);
        }


        if (ambient == null) {
            System.out.println("WARN: ambient null for " + getName());
            ambient = black;
        }

        if (diffuse == null) {
            System.out.println("WARN: diffuse null for " + getName());
            diffuse = black;
        }

        if (specular == null) {
            System.out.println("WARN: specular null for " + getName());
            specular = black;
        }

        if (emissive == null) {
            emissive = black;
        }

        mat.setFloat("Shininess", shininess);
        mat.setColor("Ambient", ambient);
        mat.setColor("Diffuse", diffuse);
        mat.setColor("Specular", specular);
        mat.setBoolean("UseMaterialColors", true);

        cachedMaterial = new WeakReference<Material>(mat);
        MaterialManager.getInstance().addMaterial(bitmap, mat);
        return mat;

//
//		Material m = new Material(ambient, emissive, diffuse, specular, shininess);
//
//		m.setLightingEnable(true);
//		a.setMaterial(m);
//
//		TextureUnitState main = null;
//
//		Texture mainTex = findTexture(base, getBitmap());
//
//		if (mainTex != null) {
//			TextureAttributes texAttr2 = new TextureAttributes();
//
//			texAttr2.setTextureMode(TextureAttributes.MODULATE);
//
//			// add disabled tex coord generation for directx workaround
//			TexCoordGeneration tcg = new TexCoordGeneration();
//
//			tcg.setEnable(false);
//			main = new TextureUnitState(mainTex, texAttr2, tcg);
//		}
//
//		if (metallic) {
//
//			Texture tex = findTexture(base, "chrome1");
//
//			TexCoordGeneration tcg = new TexCoordGeneration(TexCoordGeneration.SPHERE_MAP, TexCoordGeneration.TEXTURE_COORDINATE_2);
//
//			TextureAttributes ta = new TextureAttributes();
//
//			ta.setTextureMode(TextureAttributes.COMBINE);
//			ta.setCombineRgbMode(TextureAttributes.COMBINE_INTERPOLATE);
//
//			ta.setCombineRgbSource(0, TextureAttributes.COMBINE_PREVIOUS_TEXTURE_UNIT_STATE);
//			ta.setCombineRgbFunction(0, TextureAttributes.COMBINE_SRC_COLOR);
//			ta.setCombineRgbSource(1, TextureAttributes.COMBINE_TEXTURE_COLOR);
//			ta.setCombineRgbFunction(1, TextureAttributes.COMBINE_SRC_COLOR);
//			ta.setCombineRgbSource(2, TextureAttributes.COMBINE_PREVIOUS_TEXTURE_UNIT_STATE);
//			ta.setCombineRgbFunction(2, TextureAttributes.COMBINE_SRC_ALPHA);
//
//			TextureUnitState chrome = new TextureUnitState(tex, ta, tcg);
//
//			a.setTextureUnitState(new TextureUnitState[] { main, chrome });
//
//			/*
//			 * TextureAttributes ta = new TextureAttributes();
//			 * ta.setTextureMode(TextureAttributes.MODULATE); TextureUnitState
//			 * chrome = new TextureUnitState(tex, ta, tcg);
//			 * 
//			 * 
//			 * ta = new TextureAttributes();
//			 * 
//			 * ta.setTextureMode(TextureAttributes.COMBINE);
//			 * ta.setCombineRgbMode(TextureAttributes.COMBINE_INTERPOLATE);
//			 * ta.setCombineRgbSource
//			 * (0,TextureAttributes.COMBINE_PREVIOUS_TEXTURE_UNIT_STATE);
//			 * ta.setCombineRgbFunction(0,TextureAttributes.COMBINE_SRC_COLOR);
//			 * ta
//			 * .setCombineRgbSource(1,TextureAttributes.COMBINE_TEXTURE_COLOR);
//			 * ta.setCombineRgbFunction(1,TextureAttributes.COMBINE_SRC_COLOR);
//			 * ta
//			 * .setCombineRgbSource(2,TextureAttributes.COMBINE_TEXTURE_COLOR);
//			 * ta.setCombineRgbFunction(2,TextureAttributes.
//			 * COMBINE_ONE_MINUS_SRC_ALPHA); TexCoordGeneration ntcg = new
//			 * TexCoordGeneration(); ntcg.setEnable(false);
//			 * 
//			 * TextureUnitState finish = new TextureUnitState(mainTex,ta,ntcg);
//			 * 
//			 * a.setTextureUnitState(new TextureUnitState[] { main, chrome,
//			 * finish } );
//			 */
//
//		} else {
//
//			a.setTextureUnitState(new TextureUnitState[] { main });
//		}
//
//		if (alpha < 1 || (!metallic && (mainTex != null && mainTex.getFormat() == Texture.RGBA))) {
//			a.setTransparencyAttributes(new TransparencyAttributes(TransparencyAttributes.BLENDED, 0.01f));
//			main.getTextureAttributes().setTextureMode(TextureAttributes.REPLACE);
//		}
//
//		return a;
    }

    public static Texture findTexture(AssetManager base, String bitmap) {

        if (bitmap == null) {
            bitmap = "Textures/ColoredTex/Monkey.png";
        } else {
            if (!bitmap.endsWith(".tga")) {
                bitmap = bitmap + ".tga";
            }
        }

        Texture tex_ml = base.loadTexture(bitmap);
        return tex_ml;
    }

    public Geometry createShape(AssetManager base, ShadingType metallic) {
        Geometry gm = new Geometry(getName(), createGeometry(metallic));
        if(batchHint != null){
            gm.setBatchHint(batchHint);
        }
            
        gm.setMaterial(createAppearance(base, metallic));
        if (transparencyHint > 0) {
            gm.setQueueBucket(Bucket.Transparent);
        }
        return gm;
    }

    public boolean hasShape() {
        if (!render) {
            return false;
        }
        if (verts.isEmpty()) {
            System.out.println("WARN: No vertices for trimesh " + getName());
            return false;
        }
        if (faces.isEmpty()) {
            System.out.println("WARN: No faces for trimesh " + getName());
            return false;
        }
        return true;
    }

    @Override
    public Node createSingleTG(AssetManager base, ShadingType metallic) {
        Node tg = super.createSingleTG(base, metallic);

        if (hasShape()) {
            tg.attachChild(createShape(base, metallic));
            tg.setShadowMode(ShadowMode.Cast);

        }

        return tg;
    }

    /**
     * Gets the emissive.
     * 
     * @return Returns a ColorRGBA
     */
    public ColorRGBA getEmissive() {
        return emissive;
    }

    /**
     * Sets the emissive.
     * 
     * @param emissive
     *            The emissive to set
     */
    public void setEmissive(ColorRGBA emissive) {
        this.emissive = emissive;
    }

    /**
     * Gets the alpha.
     * 
     * @return Returns a float
     */
    public float getAlpha() {
        return alpha;
    }

    /**
     * Sets the alpha.
     * 
     * @param alpha
     *            The alpha to set
     */
    public void setAlpha(float alpha) {
        this.alpha = alpha;
    }

    public void setRender(int i) {
        this.render = i > 0;
    }

    public void setTransparencyHint(int i) {
        this.transparencyHint = i;
    }
}
